前段时间公司提了一个新的需求,在商品的详情页要实现站内买家和商品卖家实时通讯的功能以方便沟通促成交易,要开发此功能当时首先考虑到的就是swoole和workerman了,从网上大概了解了一下关于这两款工具的阐述,功能都是相当强大的,考虑到项目的进度问题,还是选择上手容易比较快的GatewayWorker和框架TP5。
先看一下我们前端设计高大上的模板,分别是用户和卖家后台。 功能还是比较全的,几乎模仿的是QQ。
业务上的大概需求是,用户在进入某个商品详情页下,给用户提供一个和卖家沟通的接口,根据商品的ID找到对应的卖家,类似于淘宝,还有发送图片,发送对应的商品链接;商户后台也差不多。
我们的平台上有虚拟商品和实体商品两大分类,当时也考虑到了消息的读取状态。我的表最初设计如下,没有加任何的索引,考虑的或许也不够周全,有见地的前辈还望指点一二!
DROP TABLE IF EXISTS `hp_chat_log`;CREATE TABLE `hp_chat_log` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '聊天记录表主键id', `user_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '用户id', `merchant_id` varchar(15) COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '商家id', `send_message` text COLLATE utf8_unicode_ci NOT NULL, `send_message_type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '发送消息类型(1:普通文本;2:商品链接,3:用户发送图片)', `sender` tinyint(1) NOT NULL DEFAULT '1' COMMENT '发送方。1:用户。2:商家', `send_time` int(11) NOT NULL DEFAULT '0' COMMENT '发送时间', `read_status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '是否已读。0:未读取。1:已读取', `acc_isonline` tinyint(1) NOT NULL DEFAULT '0' COMMENT '接收方是否在线 (0:不在线;1:在线)', PRIMARY KEY (`id`)) ENGINE=MyISAM AUTO_INCREMENT=157 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
模板有了,表设计好了,接下来就是搭建服务了,当前项目开发的框架用的是TP5,选择的Websocket框架是GatewayWorker框架,关于GatewayWorker与TP5的整合方法可以看我的这篇文章,讲到了在Linux和
Windows下的整合安装。
http://www.cnblogs.com/wt645631686/p/7219519.html
整合好了之后需要根据当前服务器的一些端口配置在修改一些默认的配置,因为需要客户端通过指定的端口建立连接。
TP5整合好了之后Gateway和workerman的主体目录结构都在TP5的框架目录vendor下的workerman目录下。需要修改里面gateway目录下的一些文件的端口及IP地址配置。
配置完成之后,进入项目目录,按照workerman官方手册提供的使用方法,用命令php start.php start启动socket服务,如以下截图,分别是1238和8282端口。当然可以在后台运行,详细的使用方法请参考手册。
启动好了之后那么就需要在客户端开始下手了,我们项目里是在前端页面里用建立的链接。看前端代码
当前的所有代码并不是最终的,目前只是阶段性开发,后期在项目中逐步完善。
var ws; // 连接服务端 function connect() { // 创建websocket ws = new WebSocket("ws://"+document.domain+":8282"); //这里如果使用127.0.0.1或者localhost会出现连接失败。当时为了方便以后的维护,这里在php的全局文件里定义了一个常量来定义ip,后来本地开发完提交到linux服务器环境之后发现链接失败!按照此行代码会有效连接~ console.log(ws); ws.onopen = onopen; ws.onmessage = onmessage; ws.onclose = function(e) { console.log(e); console.log("连接关闭,定时重连"); connect(); }; ws.onerror = function(e) { console.log(e); console.log("出现错误"); }; } // 握手 function onopen() { var joint = '{"type":"handshake","role":"user"}'; ws.send(joint); } // 服务端发来消息时 function onmessage(e) { var data = JSON.parse(e.data); console.log(data); switch(data['type']){ // 服务端ping客户端 case 'ping': ws.send('{"type":"pong"}'); break; // 登录 更新用户列表 case 'handshake': bindUid(data.client_id); $('#client_id').val(data.client_id); break; // 提醒 case 'reception': //{"type":"say","from_client_id":xxx,"to_client_id":"all/client_id","content":"xxx","time":"xxx"} warn(data['content'], data['time'], data['timestamp']); break; } } //绑定uid function bindUid (client_id) { var bindUrl = "{:url('push/push/BindUserClientId')}"; $.post(bindUrl, {client_id: client_id}, function(data){ console.log(data); }, 'json'); } //发送连接 function sendLink () { sendTrigger('link'); } // 发送信息 function sendMessage (){ sendTrigger('message'); } function sendTrigger(sendType) { var toMid = $('#toMid').val(); var pid = $('#pid').val(); var message = $("footer .send_content").val(); var client_id = $('#client_id').val(); var sendUrl = "{:url('push/push/SendMessageToMerchant')}"; $.ajax({ url:sendUrl, type:'POST', data:{message:message,toMid:toMid,pid:pid,client_id:client_id,sendType:sendType}, async:false, dataType:'JSON', success:function(data){ data = JSON.parse(data); if (data.status < 0) { alert('发送失败,请稍后再试!'); } else { $('#send_timestamp').val(data.timeStamp); $('#send_timestr').val(data.timeStr); if (sendType == 'link') { $('#main').append(data.html); } } } }) } // 提醒 function warn(content, time, prevTmestamp){ var V_image = $('#V_image').val(); var str = '' + timestampWarn(prevTmestamp, time) + '' + content + ''; domChange(str); $("#main").scrollTop($("#main")[0].scrollHeight); } //发送 function sender(content, time, prevTmestamp) { var user_image = $('#user_image').val(); var str = '' + timestampWarn(prevTmestamp, time) + ''; domChange(str); } //消息时间控制 function timestampWarn (nowTimestamp , nowTime) { var prevTimestamp = $('#prev_timestamp').val(); $('#prev_timestamp').val(nowTimestamp); var timeOffset = 6; var accTime = ''; if ((nowTimestamp - prevTimestamp) > timeOffset) { accTime = '' + ' ' + content + '' + nowTime + '
'; } return accTime; }
在开发过程中,修改的GatewayWorker文件并不多,除了几个主要的端口及IP需要修改之外,仅仅修改一个重要的文件就够了,那就是Push模块(项目的通讯模块)同级的Events.php文件。看一下项目需求里
修改后的代码,一个方法;
/** * 当客户端发来消息时触发 * @param int $client_id 连接id * @param mixed $message 具体消息 */ public static function onMessage($client_id, $message) { $message_data = json_decode($message,true); if (!$message_data) { return ; } switch($message_data['type']) { case 'pong': return; case 'handshake': $new_message = [ 'type' => $message_data['type'], 'client_id' => $client_id, 'time' => date('H:i:s') ]; Gateway::sendToClient($client_id, json_encode($new_message)); return; case 'send': if (!isset($message_data['toClientUid'])) { throw new \Exception("toClient not set. client_ip:{$_SERVER['REMOTE_ADDR']}"); } $toUid = $message_data['toClientUid']; $message = $message_data['content']; $new_message = [ 'type' => 'reception', 'content' => $message, 'time' => date('H:i:s'), 'timestamp' => time(), 'c_type' => $message_data['c_type'], 'primary' => $message_data['Db_id'] ]; //发送者角色 $source_info = explode('_', $message_data['source']); if ($source_info[0] == 'U') { //为了安全,特意做了加密 $new_message['source'] = encrypt_hopeband($source_info[1], 'E', 'XXXXXXX'); } return Gateway::sendToUid($toUid, json_encode($new_message)); } }
然后看一下Push模块下的控制器文件,在配合前端在绑定客户端ID及发送信息做的一些处理。
class Push extends Base{ protected static $user_headimage = ''; protected static $uid = null; public function __construct () { parent::__construct(); $this->checkUserLogin(); //用户头像昵称等信息 self::$uid = session('userinfo.uid'); $user_info = Hmodel\User::getUserChatinfoById(self::$uid); self::$user_headimage = json_decode($user_info['headimgurl'],true)[0]; } public function chatAction () { $product_id = intval(input('param.pid', 0, 'int')); $toMid = Hmodel\Product::getMidByProductid($product_id); if ($toMid === false) notFund(); $productHtml = $this->returnProductData2Html($product_id, 'default'); $int_toMid = substr($toMid, 2); $V_headInfo = Pmodel\Push::getVmerchantHeadImageByVid($int_toMid); $V_headimage = is_not_empty_array($V_headInfo) ? json_decode($V_headInfo['headimgurl'])[0] :'/uploads/logo.png'; if (substr($toMid, 0, 1) == 'V') { $chatLogData = Pmodel\Push::getChatlogByUseridAndVid(self::$uid, $int_toMid); if (is_not_empty_array($chatLogData)) { $chatLog = self::chatlogData2Html($chatLogData, $V_headimage); } } elseif (substr($toMid, 0, 1) == 'E') { } $view = new View; $view->assign('pHtml', $productHtml); $view->assign('toMid', $toMid); $view->assign('pid', $product_id); $view->assign('chatlogHtml', $chatLog); $view->assign('role', 'user'); $view->assign('user_image', self::$user_headimage); $view->assign('V_image', $V_headimage); return $view->fetch(); } private static function chatlogData2Html ($data = [], $V_headimage = '') { $todayTimestamp = strtotime(date('Y-m-d')); $html = ''; foreach ($data as $k => $v) { $date = $v['send_time'] < $todayTimestamp ? date('Y/m/d H:i:s', $v['send_time']) : date('H:i:s', $v['send_time']); $time_nodes = ''; if (($data[$k]['send_time'] - $data[$k-1]['send_time']) > 180) { $time_nodes = '' .$date.'
'; } //sender->发送方 1:用户。2:商家 if ($v['sender'] == 1) { //send_message_type->发送消息类型 (1:普通文本;2:商品链接) if ($v['send_message_type'] == 1) { $html.= ''; $html.= $time_nodes; $html.= ''; }elseif ($v['send_message_type'] == 2) { $html.= $time_nodes; $product_info = json_decode($v['send_message'],true); $html.= self::productData2SendHtml($product_info); }elseif ($v['send_message_type'] == 3) { $images_arr = json_decode($v['send_message'],true); $html.= ''; $html.= '' . $v['send_message'].''; $html.= ''; $html.= $time_nodes; $html.= ''; } }else { if ($v['send_message_type'] == 1) { $html.= ''; $html.= ''; foreach ($images_arr as $v ) { $html.= ' '; } $html.= ''; $html.= ''; $html.= ''; }elseif ($v['send_message_type'] == 2) { } } } return $html; }'; $html.= '' . $v['send_message'].''; $html.= '
public function BindUserClientIdAction () { if (!Request::instance()->isPost()) { notFund(); } $bindUserid = 'U_' . session('userinfo.uid'); $client_id = input("param.client_id", 0, "string"); // 设置GatewayWorker服务的Register服务ip和端口 Gateway::$registerAddress = SOCKET_SERVER_PORT; // client_id与uid绑定 // Gateway::closeClient($client_id); return Gateway::bindUid($client_id, $bindUserid);}//用户发送消息给商家 public function SendMessageToMerchantAction () { if (!Request::instance()->isPost()) { notFund(); } $message = $_POST['message']; $toMid = input('post.toMid', '' , 'string'); $product_id= input('post.pid', 0, 'int'); $client_id = input('post.client_id', '', 'string'); $sendType = input('post.sendType', '', 'string'); if (!in_array($sendType,['link', 'message'])) { //客户端错误 return json_encode(['status' => -1]); } if (strlen($client_id) != 20 ) { //客户端错误 return json_encode(['status' => -1]); } if (!is_not_empty_string($toMid) || !is_positive_integer($product_id)) { //系统错误 return json_encode(['status' => -2]); } $db_toMid = Hmodel\Product::getMidByProductid($product_id); //数据错误 if ($db_toMid != $toMid) { return json_encode(['status' => -3]); } require_once dirname(dirname(__FILE__)) . '/Events.php'; $uid = session('userinfo.uid'); $accIsOnline = Gateway::isUidOnline($toMid) == 1 ? 1 : 0; //判读商家是否在线 $message_type = 1; if ($sendType == 'link') { $message_type = 2; $productData = $this->referProductData($product_id); unset($productData['product_price']); unset($productData['score']); unset($productData['product_stock']); unset($productData['product_param']); unset($productData['product_desc']); unset($productData['product_main']); unset($productData['category_id']); unset($productData['merchant_id']); $message = json_encode($productData); } //Log入库 $insertId = Pmodel\Push::addChatLog($uid, $toMid, $message, $message_type, 1, $accIsOnline); if($message_type == 1){ if(!is_numeric($message)){ $message = '"'.$message.'"'; } if ($message == '') { $message = ''; } } if ($insertId === false) { //入库失败(服务器故障) return json_encode(['status' => -3]); } $Worker = new \Events; $message_json = '{"type":"send","source":"U_' . $uid . '","toClientUid":"' . $toMid . '","content":' . $message .', "c_type": ' . $message_type .', "Db_id":' . $insertId . '}'; $Worker::onMessage($client_id, $message_json); //成功返回相关数据 return json_encode([ 'status' => 1, 'timeStamp' => time(), 'timeStr' => date('H:i:s'), 'html' => $message_type == 1 ? '' : self::productData2SendHtml($productData) ]); }//商家发送信息给用户 public function sendMessageToUserAction () { if (!Request::instance()->isPost()) { notFund(); } $post_message = is_not_empty_string($_POST['message']) ? $_POST['message'] : ''; $toUserCode = input('post.toUserCode', '' , 'string'); $toU_uid = encrypt_hopeband($toUserCode, 'D', 'xxxxx'); $V_client_id = input('post.client_id', '', 'string'); $V_uid_code = input('post.myCode', '', 'string'); $V_uid = encrypt_hopeband($V_uid_code, 'D', 'xxxxx'); $make_message = []; $message = ''; self::trimImageAndTextinfo2str($post_message, $make_message); if (is_not_empty_array($make_message)) { foreach ( $make_message as &$v ) { $message .= self::checkIflegalAndReturn($v); } } if (strlen($V_client_id) != 20 || Gateway::isOnline($V_client_id) != 1) { //客户端错误 return json_encode(['status' => -2]); } $V_merchantInfo = Pmodel\Push::getVmerchantInfoByVid($V_uid); if (!is_not_empty_array($V_merchantInfo)) { //商家信息不存在 return json_encode(['status' => -1]); } require_once dirname(dirname(__FILE__)) . '/Events.php'; $accIsOnline = Gateway::isUidOnline('U_' . $toU_uid) == 1 ? 1 : 0; //判读用户是否在线 $message_type = 1; //Log入库 $insertId = Pmodel\Push::addChatLog($toU_uid, 'V_' . $V_uid, $message, 1, 2, $accIsOnline); if ($insertId === false) { //入库失败(服务器故障) return json_encode(['status' => -3]); } $Worker = new \Events; $img_encrypt_code = encrypt_hopeband('Hp_(legal)', 'E', 'Hp_HopeBand_Chat_img'); $message = str_replace($img_encrypt_code .' src="', $img_encrypt_code . " src='", $message); $message = str_replace('">', "'>", $message); $message_json = '{"type":"send","toClientUid":"U_' . $toU_uid . '","content":"' . $message .'","Db_id": "' . $insertId . '"}'; $Worker::onMessage($client_id, $message_json); //成功返回相关数据 return json_encode([ 'status' => 1, 'timeStamp' => time(), 'timeStr' => date('H:i:s'), ]); }
额外还有一些关于消息处理方面的;
//验证是否是否是图片,如果是并且返回图片地址,否则返回字符串 private static function checkIflegalAndReturn ( $message = '') { header('content-type:text/html; charset=utf-8'); $img_encrypt_code = encrypt_hopeband('Hp_(legal)', 'E', 'Hp_HopeBand_Chat_img'); if (substr($message, 0, 21) != '') { return $message; } $preg = '//is'; preg_match( $preg, $message,$arr); $img_src = self::base64_upload($arr[1]); return ' '; } //把接受到的消息文本和图片有序提出并解析 private static function trimImageAndTextinfo2str ($message = '', &$message_arr = []) { if (!is_not_empty_string($message)) return ''; $img_start_code = ' '; $tmp_message = strlen($message); $initial = substr($message,0,strlen($img_start_code)); if ($initial == $img_start_code) { $start = strpos($message, $img_start_code, 0); $end = strpos($message, $img_end_code , 0); $message_arr[] = substr($message, 0, $end + 2); $message = substr($message, $end + 2); }else{ $start = strpos($message, $img_start_code); if ($start !== false) { $message_arr[] = substr($message, 0, $start); $message = substr($message, $start); }else{ //防止xss攻击 $message_arr[]= string_remove_xss(htmlspecialchars_decode($message)); } } if (($tmp_message) != strlen($message) && is_not_empty_string($message)) { self::trimImageAndTextinfo2str($message, $message_arr); } return $message_arr; } private static function base64_upload($base64 = '') { $base64_image = str_replace(' ', '+', $base64); if (preg_match('/^(data:\s*image\/(\w+);base64,)/', $base64_image, $result)){ if($result[2] == 'jpeg'){ $image_name = getCode(16,4).'.jpg'; }else{ $image_name = getCode(16,4).'.'.$result[2]; } $image_file = "./upload/chat".'/'.date('Y').'/'.date('m').'/'.date('d').'/'.$image_name; //服务器文件存储路径 //判断文件路径是否存在 $path = "./upload/chat".'/'.date('Y').'/'.date('m').'/'.date('d').'/'; is_dir($path) or mkdir($path,0777,true); if (file_put_contents($image_file, base64_decode(str_replace($result[1], '', $base64_image)))){ return $image_file; }else{ return false; } }else{ return false; } }
微信的图片上传
JS部分
/* 退款选择图片 -------- start */ $("#chooseImage").click(function(){ wx.chooseImage( { count: 9, // 默认9 sizeType: ['original','compressed'], // 可以指定是原图还是压缩图,默认二者都有 sourceType: ['album', 'camera'], // 可以指定来源是相册还是相机,默认二者都有 success: function (res) { images.localId = res.localIds; // 返回选定照片的本地ID列表,localId可以作为img标签的src属性显示图片 //for(var j = 0;j // ////' // $("#main").append(str); // } var t = 0; var i = 0, length = images.localId.length; images.serverId = []; /* upload 方法 -------- start */ function upload() { wx.uploadImage( { localId: images.localId[i], success: function (res) { i++; images.serverId.push(res.serverId); if (i < length) { upload(); } var str = '' $("#main").append(str); if(i >= length ) uploadImageToDb(images.serverId); }, fail: function (res){ } }); } /* upload 方法 -------- end */ upload(); } }) }); function uploadImageToDb(images){ var str = ""; var upUrl = "http://xxxxxx.com/push/push/uploadImgage"; var toMid = $('#toMid').val(); var client_id = $('#client_id').val(); $.post(upUrl,{images:images,toMid:toMid,client_id:client_id},function(data){ if(data == 1){ for(var n = 0 ; n < $(".chat-sender").length ; n++){ str = $(".chat-sender").eq(n).attr("serverId")+","; for(var z=0;z上传失败'); } } } } }) } /* 退款选择图片 -------- end */
后台部分
//微信上传图片 public function uploadImgageAction () { if (!Request::instance()->isPost()) { notFund(); } $images = $_POST['images']; if (empty($images)) die; $toMid = input('post.toMid', '' , 'string'); $client_id = input('post.client_id', '', 'string'); if (strlen($client_id) != 20 ) { //客户端错误 return json_encode(['status' => -1]); } if (!is_not_empty_string($toMid)) { //系统错误 return json_encode(['status' => -2]); } require_once dirname(dirname(__FILE__)) . '/Events.php'; $accIsOnline = Gateway::isUidOnline($toMid) == 1 ? 1 : 0; //判读商家是否在线 $message_type = 3; //微信上传图片处理Start $res = json_decode(file_get_contents("access_token.json")); foreach ($res as $key => $value) { if($key == 'access_token'){ $access_token = $value; } } $data = []; foreach ($images as $k => $v) { $str = date('YmdHis').rand(1000,9999).'.jpg'; $targetName = './upload/chat/'.$str; if (!file_exists("./upload/chat/")) { mkdir("./upload/chat/", 0777, true); } $ch = curl_init("http://file.api.weixin.qq.com/cgi-bin/media/get?access_token=".$access_token."&media_id=".$v); $fp = fopen($targetName, 'wb'); curl_setopt($ch, CURLOPT_FILE, $fp); curl_setopt($ch, CURLOPT_HEADER, 0); $msg["status"] = curl_exec($ch); $msg["filename"] = $str; curl_close($ch); fclose($fp); $data[] = $targetName; } //微信上传图片处理End if (!is_not_empty_array($data)) { //微信服务器端图片上传错误 return json_encode(['status' => -2]); } $message = json_encode($data); //Log入库 $insertId = Pmodel\Push::addChatLog(self::$uid, $toMid, $message, $message_type, 1, $accIsOnline); if ($insertId === false) { //入库失败(服务器故障) return json_encode(['status' => -3]); } $Worker = new \Events; $message_json = '{"type":"send","source":"U_' . self::$uid . '","toClientUid":"' . $toMid . '","content":' . $message .', "c_type": ' . $message_type .', "Db_id":' . $insertId . '}'; $Worker::onMessage($client_id, $message_json); //成功返回相关数据 return json_encode([ 'status' => 1, 'timeStamp' => time(), 'timeStr' => date('H:i:s') ]); }
其他一些不是很重要的代码就不拿出来了。
当前项目只是一个简单的需求,并没有把GatewayWorker很多强大的功能体现出来,大家以后在项目开发中遇到更为复杂的需求,参考官方手册提供的一些Demo就可以慢慢实现并开发出更为健壮的项目!